﻿using System;
using System.Linq;
using System.Web;
using System.Web.Security;
using System.Security;
using FutureVision.Infrastructure;

namespace VietThinks.Security.Web
{
    public abstract class FormsAuthenticationServiceBase : IAuthenticationService
    {
        public const string AuthenticationType = "FormsAuthentication";
        public static readonly string AuthenticationCookieName;
        public static readonly string AuthenticationCookiePath;

        static FormsAuthenticationServiceBase()
        {
            AuthenticationCookieName = FormsAuthentication.FormsCookieName;
            AuthenticationCookiePath = FormsAuthentication.FormsCookiePath;
        }

        #region Static members

        /// <summary>
        /// Creates user principal from client's authentication ticket, then attaches it to current Http context
        /// </summary>
        public static void PrepareContextPrincipal()
        {
            if (!(HttpContext.Current.User is IUserPrincipal))
            {
                HttpContext.Current.User = CreatePrincipalFromAuthenticationTicket();
            }
        }

        /// <summary>
        /// Creates authentication ticket from the given user principal and embeds it into response's cookie
        /// </summary>
        /// <param name="user">User principal</param>
        /// <param name="expirationWindow">Ticket's timeout</param>
        /// <param name="persistsAuthentication">Ticket's session-cross persistence</param>
        protected static void CreateAuthenticationTicketFromPrincipal(IUserPrincipal user, TimeSpan expirationWindow, bool persistsAuthentication)
        {
            var ticket = new FormsAuthenticationTicket(
                     1,                                        // version
                     user.Identity.Name,                       // id
                     DateTime.Now,                             // issue time
                     DateTime.Now.Add(expirationWindow),       // expiration time
                     persistsAuthentication,                   // persistent
                     String.Format("{0}:{1}:{2}:{3}", user.UserIdentity.Id, user.UserIdentity.IsAuthenticated, String.Join(",", user.Roles.ToArray()), user.UserIdentity.FullName),
                     FormsAuthentication.FormsCookiePath
            );

            var cookie = new HttpCookie(FormsAuthentication.FormsCookieName, FormsAuthentication.Encrypt(ticket))
            {
                Path = FormsAuthentication.FormsCookiePath,
            };
            HttpContext.Current.Response.Cookies.Add(cookie);
        }

        /// <summary>
        /// Creates user principal from request's authentication ticket and attaches it into current HttpContext
        /// </summary>
        protected static IUserPrincipal CreatePrincipalFromAuthenticationTicket()
        {   
            var authenticationCookie = HttpContext.Current.Request.Cookies[AuthenticationCookieName];

            if (authenticationCookie != null)
            {
                try
                {
                    var authenticationTicket = FormsAuthentication.Decrypt(authenticationCookie.Value);

                    string colonSeperatedParts = authenticationTicket.UserData;

                    string[] parts = colonSeperatedParts.Split(':');

                    int id = Int32.Parse(parts[0]);
                    bool isAuthenticated = Boolean.Parse(parts[1]);
                    string commaSeparatedRoles = parts[2];
                    string fullName = parts[3];
                    
                    return new UserPrincipal(new UserIdentity(id, authenticationTicket.Name, fullName, AuthenticationType, isAuthenticated), 
                                             commaSeparatedRoles.Split(','));
                }
                catch // parsing error means "unauthenticated"
                {
                    // just drops
                }
            }

            return UserPrincipal.Unidentified;
        }

        #endregion
        
        public TimeSpan ExpirationTimeWindow { get; set; }
        public bool PersistsAuthentication { get; set; }

        public event UserAuthenticationEventHandler UserAuthenticating;
        public event UserAuthenticationEventHandler UserAuthenticated;
        public event UserAuthenticationEventHandler UserUnauthenticated;
        public event UserAuthenticationEventHandler UserSignedOut;

        /// <summary>
        /// Overrides this method to return user credential.
        /// </summary>
        /// <param name="userId">User Id</param>
        /// <param name="password">Passwlrd</param>
        /// <returns>IUserPrincipal</returns>
        protected abstract IUserPrincipal GetUserCredential(string userId, string password);

        #region Event Raiser

        protected virtual void OnSignedOut(IUserPrincipal user)
        {
            if (this.UserSignedOut != null)
            {
                this.UserSignedOut(this, new UserAuthenticationEventArgs(user));
            }
        }

        protected virtual void OnAuthenticating(IUserPrincipal user)
        {
            if (this.UserAuthenticating != null)
            {
                this.UserAuthenticating(this, new UserAuthenticationEventArgs(user));
            }
        }

        protected virtual void OnUnauthenticated(IUserPrincipal user)
        {
            if (this.UserUnauthenticated != null)
            {
                this.UserUnauthenticated(this, new UserAuthenticationEventArgs(user));
            }
        }

        protected virtual void OnAuthenticated(IUserPrincipal user)
        {
            if (this.UserAuthenticated != null)
            {
                this.UserAuthenticated(this, new UserAuthenticationEventArgs(user));
            }
        }

        #endregion

        /// <summary>
        /// Authenticates given credential. If successful, creates authenticatation ticket at client side
        /// </summary>
        /// <param name="userId">User Id</param>
        /// <param name="password">Password</param>
        /// <param name="expirationTimeWindow">Time before the authentication ticket expires</param>
        /// <param name="persistsAuthentication">true if authentication ticket should never expire</param>
        /// <returns>true if authenticated, false otherwise</returns>
        public bool Authenticate(string userId, string password, TimeSpan expirationTimeWindow, bool persistsAuthentication)
        {
            IUserPrincipal user = this.GetUserCredential(userId, password);

            this.OnAuthenticating(user);

            if (user != null && user.Identity.IsAuthenticated)
            {
                CreateAuthenticationTicketFromPrincipal(user, expirationTimeWindow, persistsAuthentication);
                
                HttpContext.Current.User = user;
                
                this.OnAuthenticated(user);

                return true;
            }

            this.OnUnauthenticated(user);

            return false;
        }

        #region IAuthenticationService Members

        public bool Authenticate(string userId, string password)
        {
            return this.Authenticate(userId, password, this.ExpirationTimeWindow, this.PersistsAuthentication);
        }

        public bool Authenticate(string userId, string password, bool persistsAuthentication)
        {
            return this.Authenticate(userId, password, this.ExpirationTimeWindow, persistsAuthentication);
        }

        public void SignOut()
        {
            IUserPrincipal user = CreatePrincipalFromAuthenticationTicket();

            FormsAuthentication.SignOut();

            this.OnSignedOut(user);
        }

        #endregion
    }

    #region EventHandler and EventArgs

    public class UserAuthenticationEventArgs : EventArgs
    {
        public UserAuthenticationEventArgs(IUserPrincipal user)
        {
            this.User = user;
        }

        public IUserPrincipal User { get; private set; }
    }

    public delegate void UserAuthenticationEventHandler(object sender, UserAuthenticationEventArgs eventArgs);

    #endregion
}
